//
//  ========================================================================
//  Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.start;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.jetty.start.Props.Prop;
import org.eclipse.jetty.start.config.CommandLineConfigSource;

/**
 * Represents a Module metadata, as defined in Jetty.
 * 
 * <p>A module consists of:
 * <ul>
 * <li>A set of jar files, directories and/or jar file patterns to be added to the classpath</li>
 * <li>A list of XML configuration files</li>
 * <li>Properties set either directly or via a file of properties</li>
 * <li>A set of modules names (or capability names) that this module depends on.</li>
 * <li>A set of capability names that this module provides (including it's own name).</li>
 * <li>Licence details for using the module</li>
 * </ul>
 * Modules are discovered in the <code>${jetty.home}/modules</code> and 
 * <code>${jetty.home}/modules</code> directories. A module may refer to 
 * non-discovered dynamic module in a subdirectory, using a property as part or 
 * all of the name.
 * A module may be enabled, either directly by name or transiently via a dependency
 * from another module by name or provided capability.
 */
public class Module implements Comparable<Module>
{
    private static final String VERSION_UNSPECIFIED = "9.2";
    static Pattern MOD_NAME = Pattern.compile("^(.*)\\.mod",Pattern.CASE_INSENSITIVE);
    static Pattern SET_PROPERTY = Pattern.compile("^(#?)\\s*([^=\\s]+)=(.*)$");

    /** The file of the module */
    private final Path _path;

    /** The name of the module */
    private final String _name;
    
    /** Is the module dynamic - ie referenced rather than discovered */
    private final boolean _dynamic;

    /** The version of Jetty the module supports */
    private Version version;
    
    /** The module description */
    private final List<String> _description=new ArrayList<>();

    /** List of xml configurations for this Module */
    private final List<String> _xmls=new ArrayList<>();
    
    /** List of ini template lines */
    private final List<String> _iniTemplate=new ArrayList<>();
    
    /** List of default config */
    private final List<String> _defaultConfig=new ArrayList<>();
    
    /** List of library options for this Module */
    private final List<String> _libs=new ArrayList<>();
    
    /** List of files for this Module */
    private final List<String> _files=new ArrayList<>();
    
    /** List of selections for this Module */
    private final Set<String> _enables=new HashSet<>();
    
    /** List of provides for this Module */
    private final Set<String> _provides=new HashSet<>();
    
    /** List of tags for this Module */
    private final List<String> _tags=new ArrayList<>();
    
    /** Boolean true if directly enabled, false if all selections are transitive */
    private boolean _notTransitive;
    
    /** Skip File Validation (default: false) */
    private boolean _skipFilesValidation = false;
    
    /** List of jvm Args */
    private final List<String> _jvmArgs=new ArrayList<>();
    
    /** License lines */
    private final List<String> _license=new ArrayList<>();
    
    /** Dependencies */
    private final List<String> _depends=new ArrayList<>();
    
    /** Optional */
    private final Set<String> _optional=new HashSet<>();

    public Module(BaseHome basehome, Path path) throws FileNotFoundException, IOException
    {
        super();
        _path = path;
        
        // Module name is the / separated path below the modules directory
        int m=-1;
        for (int i=path.getNameCount();i-->0;)
        {
            if ("modules".equals(path.getName(i).toString()))
            {
                m=i;
                break;
            }
        }
        if (m<0)
            throw new IllegalArgumentException("Module not contained within modules directory: "+basehome.toShortForm(path));
        String n=path.getName(m+1).toString();
        for (int i=m+2;i<path.getNameCount();i++)
            n=n+"/"+path.getName(i).toString();
        Matcher matcher=MOD_NAME.matcher(n);
        if (!matcher.matches())
            throw new IllegalArgumentException("Module filename must have .mod extension: "+basehome.toShortForm(path));
        _name=matcher.group(1);
    
        _provides.add(_name);
        _dynamic=_name.contains("/");        
        
        process(basehome);
    }

    public String getName()
    {
        return _name;
    }
    
    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        if (obj == null)
        {
            return false;
        }
        if (getClass() != obj.getClass())
        {
            return false;
        }
        Module other = (Module)obj;
        if (_path == null)
            return other._path == null;
        
        return _path.equals(other._path);
    }

    public void expandDependencies(Props props)
    {
        Function<String,String> expander = d->{return props.expand(d);};
        
        List<String> tmp=_depends.stream().map(expander).collect(Collectors.toList());
        _depends.clear();
        _depends.addAll(tmp);
        tmp=_optional.stream().map(expander).collect(Collectors.toList());
        _optional.clear();
        _optional.addAll(tmp);
    }

    public List<String> getDefaultConfig()
    {
        return _defaultConfig;
    }
    
    public List<String> getIniTemplate()
    {
        return _iniTemplate;
    }

    public List<String> getFiles()
    {
        return _files;
    }

    public boolean isSkipFilesValidation()
    {
        return _skipFilesValidation;
    }

    public List<String> getJvmArgs()
    {
        return _jvmArgs;
    }

    public List<String> getLibs()
    {
        return _libs;
    }

    public List<String> getLicense()
    {
        return _license;
    }
    
    public List<String> getXmls()
    {
        return _xmls;
    }
    
    public Version getVersion()
    {
        return version;
    }

    public boolean hasDefaultConfig()
    {
        return !_defaultConfig.isEmpty();
    }
    
    public boolean hasIniTemplate()
    {
        return !_iniTemplate.isEmpty();
    }

    @Override
    public int hashCode()
    {
        return _name.hashCode();
    }

    public boolean hasLicense()
    {
        return (_license != null) && (_license.size() > 0);
    }

    /**
     * Indicates a module that is dynamic in nature
     * 
     * @return a module where the name is not in the top level of the modules directory
     */
    public boolean isDynamic()
    {
        return _dynamic;
    }

    public boolean hasFiles(BaseHome baseHome, Props props)
    {
        for (String ref : getFiles())
        {
            FileArg farg = new FileArg(this,props.expand(ref));
            Path refPath = baseHome.getBasePath(farg.location);
            if (!Files.exists(refPath))
            {
                return false;
            }
        }
        return true;
    }

    public void process(BaseHome basehome) throws FileNotFoundException, IOException
    {
        Pattern section = Pattern.compile("\\s*\\[([^]]*)\\]\\s*");

        if (!FS.canReadFile(_path))
        {
            StartLog.debug("Skipping read of missing file: %s",basehome.toShortForm(_path));
            return;
        }

        try (BufferedReader buf = Files.newBufferedReader(_path,StandardCharsets.UTF_8))
        {
            String sectionType = "";
            String line;
            while ((line = buf.readLine()) != null)
            {
                line = line.trim();

                Matcher sectionMatcher = section.matcher(line);

                if (sectionMatcher.matches())
                {
                    sectionType = sectionMatcher.group(1).trim().toUpperCase(Locale.ENGLISH);
                }
                else
                {
                    // blank lines and comments are valid for ini-template section
                    if ((line.length() == 0) || line.startsWith("#"))
                    {
                        // Remember ini comments and whitespace (empty lines)
                        // for the [ini-template] section
                        if ("INI-TEMPLATE".equals(sectionType))
                        {
                            _iniTemplate.add(line);
                        }
                    }
                    else
                    {
                        switch (sectionType)
                        {
                            case "":
                                // ignore (this would be entries before first section)
                                break;
                            case "DESCRIPTION":
                                _description.add(line);
                                break;
                            case "DEPEND":  
                            case "DEPENDS":
                                if (!_depends.contains(line))
                                    _depends.add(line);
                                break;
                            case "FILE":
                            case "FILES":
                                _files.add(line);
                                break;
                            case "TAG":
                            case "TAGS":
                                _tags.add(line);
                                break;
                            case "DEFAULTS": // old name introduced in 9.2.x
                            case "INI": // new name for 9.3+
                                _defaultConfig.add(line);
                                break;
                            case "INI-TEMPLATE":
                                _iniTemplate.add(line);
                                break;
                            case "LIB":
                            case "LIBS":
                                _libs.add(line);
                                break;
                            case "LICENSE":
                            case "LICENSES":
                            case "LICENCE":
                            case "LICENCES":
                                _license.add(line);
                                break;
                            case "NAME":
                                StartLog.warn("Deprecated [name] used in %s",basehome.toShortForm(_path));
                                _provides.add(line);
                                break;
                            case "PROVIDE":
                            case "PROVIDES":
                                _provides.add(line);
                                break;
                            case "OPTIONAL":
                                _optional.add(line);
                                break;
                            case "EXEC":
                                _jvmArgs.add(line);
                                break;
                            case "VERSION":
                                if (version != null)
                                {
                                    throw new IOException("[version] already specified");
                                }
                                version = new Version(line);
                                break;
                            case "XML":
                                _xmls.add(line);
                                break;
                            default:
                                throw new IOException("Unrecognized module section: [" + sectionType + "]");
                        }
                    }
                }
            }
        }
        
        if (version == null)
        {
            version = new Version(VERSION_UNSPECIFIED);
        }
    }

    public boolean clearTransitiveEnable()
    {
        if (_notTransitive)
            throw new IllegalStateException("Not Transitive");
        if (isEnabled())
        {
            _enables.clear();
            return true;
        }
        return false;
    }
    
    public void setSkipFilesValidation(boolean skipFilesValidation)
    {
        this._skipFilesValidation = skipFilesValidation;
    }
    
    @Override
    public String toString()
    {
        StringBuilder str = new StringBuilder();
        str.append(getName());
        char sep='{';
        if (isDynamic())
        {
            str.append(sep).append("dynamic");
            sep=',';
        }
        if (isEnabled())
        {
            str.append(sep).append("enabled");
            sep=',';
        }
        if (isTransitive())
        {
            str.append(sep).append("transitive");
            sep=',';
        }
        if (sep!='{')
            str.append('}');
        return str.toString();
    }

    public List<String> getDepends()
    {
        return new ArrayList<>(_depends);
    }

    public Set<String>  getProvides()
    {
        return new HashSet<>(_provides);        
    }
    
    public Set<String> getOptional()
    {
        return new HashSet<>(_optional);
    }
    
    public List<String> getDescription()
    {
        return _description;
    }
    
    public List<String> getTags()
    {
        return _tags;
    }
    
    public String getPrimaryTag()
    {
        return _tags.isEmpty()?"*":_tags.get(0);
    }
    
    public boolean isEnabled()
    {
        return !_enables.isEmpty();
    }
    
    public Set<String> getEnableSources()
    {
        return new HashSet<>(_enables);
    }

    /**
     * @param source String describing where the module was enabled from
     * @param transitive True if the enable is transitive
     * @return true if the module was not previously enabled
     */
    public boolean enable(String source,boolean transitive)
    {
        boolean updated=_enables.isEmpty();
        if (transitive)
        {
            // Ignore transitive selections if explicitly enabled
            if (!_notTransitive)
                _enables.add(source);
        }
        else
        {
            if (!_notTransitive)
            {
                // Ignore transitive selections if explicitly enabled
                updated=true;
                _enables.clear(); // clear any transitive enabling
            }
            _notTransitive=true;
            _enables.add(source);
        }
        return updated;
    }

    public boolean isTransitive()
    {
        return isEnabled() && !_notTransitive;
    }
    
    public void writeIniSection(BufferedWriter writer, Props props)
    {
        PrintWriter out = new PrintWriter(writer);
        out.println("# --------------------------------------- ");
        out.println("# Module: " + getName());
        for (String line : getDescription())
            out.append("# ").println(line);
        out.println("# --------------------------------------- ");
        out.println("--module=" + getName());
        out.println();
        for (String line : getIniTemplate())
        {
            Matcher m = SET_PROPERTY.matcher(line);
            if (m.matches() && m.groupCount()==3)
            {
                String name = m.group(2);
                Prop p = props.getProp(name);
                if (p!=null && p.origin.startsWith(CommandLineConfigSource.ORIGIN_CMD_LINE))
                {
                    StartLog.info("%-15s property set %s=%s",this._name,name,p.value);
                    out.printf("%s=%s%n",name,p.value);
                }
                else
                    out.println(line);
            }
            else
                out.println(line);
        }
        out.println();
        out.flush();
    }

    @Override
    public int compareTo(Module m)
    {
        int by_tag = getPrimaryTag().compareTo(m.getPrimaryTag());
        if (by_tag!=0)
            return by_tag;
        return getName().compareTo(m.getName());
    }
}
